﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections;
using ServerServices;
using System.Runtime.Remoting.Channels.Tcp;
using System.Runtime.Remoting;
using CommonLib;
using System.Configuration;
using System.Timers;

namespace PadiCalClient
{
    class Agenda : MarshalByRefObject, IClientService
    {

        private string[] servers = { "central-1", "central-2", "central-3" };

        private int counter = 1;

        public Slot[] slots = new Slot[8761];

        IServerService serverOperations;

        public IClientService clientOperations;

        IDictionary<string, User> knownUsers = new Dictionary<string, User>();

        private User _user;

        private ArrayList pendingReservations = new ArrayList();

        private TcpChannel tcp;

        //private ArrayList abortAnnounces = new ArrayList();

        private Timer cleaner = new Timer(Int32.Parse(ConfigurationSettings.AppSettings["cleaner_timeout"]));

        private ArrayList waitingReservations = new ArrayList();

        private ArrayList initReservations = new ArrayList();

        private ArrayList waitingAcks = new ArrayList();

        private System.Threading.Thread treat;

        private bool treatRunning;

        public Agenda(string User, TcpChannel t)
        {
            cleaner.Elapsed += new ElapsedEventHandler(clean_Event);

            cleaner.Start();

            this.tcp = t;

            for (int i = 0; i < 8761; i++)
            {
                slots[i] = new Slot(i, this);
            }

            treat = new System.Threading.Thread(new System.Threading.ThreadStart(Treater));

            treat.IsBackground = true;

            treatRunning = true;

            treat.Start();

        }


        public void clean()
        {
            Slot sl;
            Reservation r;
            for (int i = 0; i < slots.Length; i++)
            {
                sl = slots[i];
                if (!sl.State.Equals("Assigned"))
                {
                    for (int j = 0; j < sl.SlotReservations.Count; j++)
                    {
                        r = (Reservation)sl.SlotReservations[j];
                        if (!r.clean_flag)
                        {
                            r.clean_flag = true;
                        }
                        else
                        {
                            Console.WriteLine("Cleaner removed reservation {0} in slot {1}", r.Name, i);

                            if (r.getHostName().Equals(_user.Name))
                            {
                                int index;
                                if ((index = findPending(r.getID())) != -1)
                                {
                                    pendingReservations.RemoveAt(findPending(r.getID()));
                                }
                            }
                            abortEntireReservation(r);
                            j--;
                        }
                    }
                }
            }
        }

        private void clean_Event(object source, ElapsedEventArgs e)
        {
            clean();
        }

        public User user
        {
            set
            {
                this._user = value;
            }
            get
            {
                return this._user;
            }
        }

        public string[] getCalendar()
        {
            String[] ret = new String[8761];
            for (int i = 0; i < 8761; i++)
            {
                ret[i] = slots[i].State;
            }
            return ret;
        }

        private IServerService chooseServer()
        {

            //Random r = new Random();

            //int choosen = r.Next(servers.Length);

            //string serverAddress = ConfigurationSettings.AppSettings[servers[choosen]];
            string serverAddress = ConfigurationSettings.AppSettings["central-1"];
            return (IServerService)Activator.GetObject(typeof(IServerService),
            "tcp://" + serverAddress + "/ServerService");

        }

        public void receivedInitReservation(InitReservation init)
        {
            lock (initReservations)
            {
                initReservations.Add(init);
            }
            if (!treatRunning)
            {
                treatRunning = true;
                treat.Resume();
            }
        }


        private void beginReservation(string name, ArrayList users, ArrayList Rslots)
        {

            Rslots = checkAvailability(Rslots);
            if (Rslots.Count == 0)

                Console.WriteLine("There is no available slots for the reservation! Reservation aborted!");

            else
            {
                Reservation newReservation = new Reservation(name, users, Rslots, _user, counter++);

                Console.WriteLine("A efetuar Reserva " + newReservation.Name);
                if (users.Count == 1)
                {
                    /* Single user reservation */
                    newReservation.changeToCommited();
                    newReservation.currentSlot = (string)Rslots[0];
                    slots[Int32.Parse((string)Rslots[0])].commitReservation(newReservation);
                }
                else
                {
                    /** Multiple user reservation **/
                    AcknowledgeSlots(Rslots, newReservation);
                    pendingReservations.Add(new pending(newReservation));
                    propagateReservation(newReservation);
                }
            }

        }

        public void receiveReservation(Reservation res)
        {
            lock (waitingReservations)
            {
                waitingReservations.Add(res);
            }
            if (!treatRunning)
            {
                treatRunning = true;
                treat.Resume();
            }
        }


        private void Treater()
        {
            while (true)
            {
                if (initReservations.Count != 0)
                {
                    InitReservation init;
                    lock (initReservations)
                    {
                        init = (InitReservation)initReservations[0];
                        initReservations.RemoveAt(0);
                    }
                    beginReservation(init.Name, init.Users, init.Slots);
                }


                if (waitingReservations.Count != 0)
                {
                    Reservation res;
                    lock (waitingReservations)
                    {
                        res = (Reservation)waitingReservations[0];
                        waitingReservations.RemoveAt(0);
                    }
                    TreatReservation(res);
                }

                if (waitingAcks.Count != 0)
                {
                    Ack ack;
                    lock (waitingAcks)
                    {
                        ack = (Ack)waitingAcks[0];
                        waitingAcks.RemoveAt(0);
                    }
                    if(ack.Reservation.State.Equals("Initiated"))
                    {
                        collectInitAcks(ack.User, ack.Reservation);
                    }
                    else if (ack.Reservation.State.Equals("Tentatively"))
                    {
                        collectTentatively(ack.Reservation, ack.User);
                    }
                    else if (ack.Reservation.State.Equals("Commit"))
                    {
                        collectCommits(ack.Reservation, ack.User);
                    }
                }
                if (waitingReservations.Count == 0 &&
                    initReservations.Count == 0 &&
                    waitingAcks.Count == 0)
                {
                    treatRunning = false;
                    treat.Suspend();
                }
            }
        }

        public void collectAck(Ack ack)
        {
            lock (waitingAcks)
            {
                waitingAcks.Add(ack);
            }
            if (!treatRunning)
            {
                treatRunning = true;
                treat.Resume();
            }
        }

        private void TreatReservation(Reservation res)
        {
            /** Save Addresses **/
            if (!knownUsers.ContainsKey(res.getHostName()))
            {
                knownUsers.Add(res.getHostName(), res.Host);
            }

            foreach (KeyValuePair<string, User> u in res.getUsers())
            {
                if (!knownUsers.ContainsKey(u.Key))
                {
                    knownUsers.Add(u.Key, u.Value);
                }
            }

            Console.WriteLine("Received " + res.Name + " in " + res.State + " in " + res.currentSlot);
            bool propagation = false;

            if (res.Propagation.Count != 0 || res.getHostName().Equals(_user.Name))
                propagation = true;

            if (res.State.Equals("Initiated"))
            {

                ArrayList availableSlots = checkAvailability(res.Slots);

                if (availableSlots.Count != 0)
                {
                    res.Slots = availableSlots;
                    AcknowledgeSlots(availableSlots, res);
                    Ack ack = new Ack(_user.Name, res);
                    if (!sendAck(ack, res.getHostName()))
                    {
                        abortEntireReservation(res);
                    }
                }
                else
                {
                    propagation = false;
                    res = initiateAbortion(res, null);

                }
            }
            else if (res.State.Equals("Tentatively"))
            {
                string Rstate = res.State;
                string Rslot = res.currentSlot;

                if (!this.slots[Int32.Parse(Rslot)].State.Equals("Free") &&
                    !this.slots[Int32.Parse(Rslot)].State.Equals("Assigned") &&
                    confirmAcknowledgeReservation(res))
                {

                    // Cancelar nacked slots
                    ArrayList oldSlots = this.slots[Int32.Parse(Rslot)].getReservation(res.getID(), res.getHostName()).Slots;
                    cancelNacked(oldSlots, res);

                    // passar slot para Booked
                    this.slots[Int32.Parse(Rslot)].insertTentativeRes(res);

                    /* Ack host */
                    Ack ack = new Ack(_user.Name, res);
                    if (!sendAck(ack, res.getHostName()))
                    {
                        /* Abort reservation - Host disconected */
                        res.State = "Aborted";
                        res.abortAll = true;
                        abortEntireReservation(res);
                    }
                }
                else
                {
                    /* Abort reservation - Slot unavailable*/
                    res.toAbort = res.currentSlot;
                    res = initiateAbortion(res, res.currentSlot);
                }

            }
            else if (res.State.Equals("Aborted"))
            {
                if (res.getHostName().Equals(_user.Name) && findPending(res.getID()) == -1)
                {
                    Console.WriteLine("Reservation {0} already aborted... Ignored!", res.Name);
                }
                else
                {
                    abortReservation(res);
                }
            }
            else if (res.State.Equals("Commit"))
            {
                if (!slots[Int32.Parse(res.currentSlot)].tryCommit(res))
                {
                    initiateAbortion(res, res.currentSlot);
                }
            }
            else if (res.State.Equals("Commited"))
            {
                Slot sl = slots[Int32.Parse(res.currentSlot)];

                /* Check if reservation exists on slots */
                Reservation slRes = sl.getReservation(res.getID(), res.getHostName());
                if (slRes == null)
                {
                    Console.WriteLine("Reservation do not exists in slot {0}... Ignored!", res.currentSlot);
                }
                else
                {
                    /* Check if resevation is in commit state in slot */
                    if (sl.getReservation(res.getID(), res.getHostName()).State.Equals("Commit"))
                    {
                        sl.commitReservation(res);
                    }
                    else
                    {
                        initiateAbortion(res, res.currentSlot);
                    }
                }
            }

            if (propagation)
                propagateReservation(res);

        }

        /* Send an intent fo abort to the reservation host */
        public Reservation initiateAbortion(Reservation res, string slot)
        {
            res.State = "Aborted";

            if (slot == null)
            {
                Console.WriteLine("Initiated abortion of {0} in all slots", res.Name);
                res.abortAll = true;
            }
            else
            {
                Console.WriteLine("Initiated abortion of {0} in slot {1}", res.Name, slot);
                res.toAbort = slot;
            }

            abortReservation(res);

            if (!knownUsers.ContainsKey(res.Host.Name))
            {
                knownUsers.Add(res.Host.Name, res.Host);
            }

            if (!sendReservation(res, res.getHostName()))
            {
                /* Abort Reservation - host disconnectd */
                abortEntireReservation(res);
            }
            return res;

        }

        private void abortReservation(Reservation res)
        {
            if (res.Host.Name.Equals(_user.Name))
            {
                if (res.abortAll)
                {
                    abortEntireReservation(res);
                    int index;
                    if ((index = findPending(res.getID())) != -1)
                    {
                        pendingReservations.RemoveAt(findPending(res.getID()));
                    }
                }
                else
                {
                    this.slots[Int32.Parse(res.toAbort)].removeReservation(res.getID(), res.getHostName());
                    Reservation newRes = ((pending)this.pendingReservations[findPending(res.getID())]).removeTentatively();
                    if (newRes != null)
                    {
                        propagateReservation(newRes);
                    }
                    else
                    {
                        this.pendingReservations.RemoveAt(findPending(res.getID()));
                    }
                }
            }
            else
            {
                /* Someone aborted a reservation */
                if (res.abortAll)
                {
                    abortEntireReservation(res);
                }
                else
                {
                    slots[Int32.Parse(res.toAbort)].removeReservation(res.getID(), res.getHostName());
                }
            }
        }

        public void abortEntireReservation(Reservation res)
        {
            if (res.currentSlot != null)
            {
                slots[Int32.Parse(res.currentSlot)].removeReservation(res.getID(), res.getHostName());
            }

            foreach (string sl in res.Slots)
            {
                slots[Int32.Parse(sl)].removeReservation(res.getID(), res.getHostName());
            }
        }


        /* Ainda n foi testado */
        private void cancelNacked(ArrayList old, Reservation New)
        {
            foreach (string sl in old)
            {
                if (!New.Slots.Contains(sl) && !New.currentSlot.Equals(sl))
                {
                    Console.WriteLine("Removed reservation " + New.Name + " from slot " + sl);
                    this.slots[Int32.Parse(sl)].removeReservation(New.getID(), New.getHostName());
                }
            }
        }

        private bool confirmAcknowledgeReservation(Reservation res)
        {
            ArrayList SlotRes = this.slots[Int32.Parse(res.currentSlot)].SlotReservations;

            Reservation r;

            foreach (Reservation sl in SlotRes)
            {
                r = sl;
                if (r.getID() == res.getID() && r.getHostName().Equals(res.getHostName()) && r.State.Equals("Initiated"))
                    return true;
            }

            return false;
        }

        private ArrayList checkAvailability(ArrayList slotList)
        {
            string Rstate;
            ArrayList result = new ArrayList();
            foreach (string sl in slotList)
            {
                Rstate = slots[Int32.Parse(sl)].State;
                if (Rstate.Equals("Free") || Rstate.Equals("Acknowledged"))
                    result.Add(sl);

            }

            return result;

        }

        private void collectCommits(Reservation ack, string Ruser)
        {
            Console.WriteLine("Received Commit Ack from " + Ruser);

            int index;
            if ((index = findPending(ack.getID())) == -1)
            {
                Console.WriteLine("Ack from a reservation that is not on pending list... Ignored!");
            }
            else
            {
                pending pen = (pending)pendingReservations[index];
                /* Check if reservation is in tentatively state */
                if (!pen.State.Equals("Commit"))
                {
                    Console.WriteLine("Reservation is not in Commit state... Ignored!");
                }
                else
                {
                    /* Collect ack */
                    pen.acked(Ruser, null);

                    if (pen.UsersAcked.Count == pen.countUsers)
                    {
                        //Assign the slot
                        slots[Int32.Parse(pen.Reservation.currentSlot)].commitReservation(pen.Reservation);
                        Reservation newReservation = pen.changeToCommited();
                        propagateReservation(newReservation);

                        if ((index = findPending(pen.getID())) != -1)
                        {
                            pendingReservations.RemoveAt(index);
                        }
                    }
                }
            }

        }

        private void collectTentatively(Reservation ack, string Ruser)
        {
            Console.WriteLine("Recieved Tentatively Ack for " + Ruser);

            int index;
            if ((index = findPending(ack.getID())) == -1)
            {
                Console.WriteLine("Ack from a reservation that is not on pending list... Ignored!");
            }
            else
            {
                pending pen = (pending)pendingReservations[index];
                /* Check if reservation is in tentatively state */
                if (!pen.State.Equals("Tentatively"))
                    Console.WriteLine("Reservation is not in Tentatively state... Ignored!");
                else
                {
                    /* Collect ack */
                    pen.acked(Ruser, null);

                    if (pen.UsersAcked.Count == pen.countUsers)
                    {
                        Reservation newReservation = pen.changeToCommit();
                        propagateReservation(newReservation);
                    }
                }
            }
        }

        private void collectInitAcks(string Ruser, Reservation ack)
        {
            Console.WriteLine("Recieved initAck from " + Ruser);
            int index;
            if ((index = findPending(ack.getID())) == -1)
                Console.WriteLine("Ack from a reservation that is not on pending list... Ignored!");
            else
            {
                pending pen = (pending)this.pendingReservations[index];

                if (!pen.State.Equals("Initiated"))
                {
                    Console.WriteLine("Ack from a reservation that is not in Initiated State... Ignored!");
                }
                else
                {
                    if (!pen.acked(Ruser, ack.Slots))
                    {
                        /* No available slot in users to succed the reservation */
                        Reservation res = pen.Reservation;
                        Console.WriteLine("Reservation {0} aborted...", res.Name);
                        res.State = "Aborted";
                        pendingReservations.Remove(pen);
                        propagateReservation(res);
                    }
                    else
                    {
                        if (pen.UsersAcked.Count == pen.countUsers)
                        {

                            ArrayList oldSlots = pen.Reservation.Slots;

                            Reservation tent = pen.changeToTentatively();

                            cancelNacked(oldSlots, tent);

                            // passar slot para Booked
                            this.slots[Int32.Parse(tent.currentSlot)].insertTentativeRes(tent);

                            propagateReservation(tent);

                        }
                    }
                }
            }
        }

        private int findPending(int id)
        {
            pending p;
            for (int i = 0; i < this.pendingReservations.Count; i++)
            {
                p = (pending)this.pendingReservations[i];
                if (p.getID() == id)
                    return i;
            }

            return -1;
        }

        /** Acknowledge slots **/
        private void AcknowledgeSlots(ArrayList slots, Reservation res)
        {
            foreach (string sl in slots)
                this.slots[Int32.Parse(sl)].TreatReservation(res);

        }


        /** Propagation method **/
        private void propagateReservation(Reservation res)
        {
            ArrayList tosend = new ArrayList();
            ArrayList part1 = new ArrayList();
            ArrayList part2 = new ArrayList();

            if (res.Host.Name.Equals(_user.Name))
            {
                tosend = res.invUsers;
                tosend.Remove(_user.Name);
            }
            else
                tosend = res.Propagation;

            if (tosend.Count == 1)
            {
                if (!knownUsers.ContainsKey((string)tosend[0]))
                {
                    if (res.hasKnownUser((string)tosend[0]))
                        this.knownUsers.Add((string)tosend[0], res.getUser((string)tosend[0]));
                    else
                    {
                        serverOperations = chooseServer();
                        User user = serverOperations.getUser((string)tosend[0]);
                        Console.WriteLine("Comunicated with the servers....");
                        if (user == null)
                        {
                            Console.WriteLine("User {0} disconected", (string)tosend[0]);
                            if (!res.State.Equals("Aborted"))
                            {
                                initiateAbortion(res, null);
                            } 
                            return;
                        }
                        else
                        {
                            knownUsers.Add((string)tosend[0], user);
                            res.addKnownUser(user);
                        }
                    }
                }
                res.Propagation = part1;
                if (!sendReservation(res, (string)tosend[0]))
                {
                    initiateAbortion(res, null);
                    return;
                }
            }
            else
            {
                if (tosend.Count - 2 > 0)
                {
                    int divider = (tosend.Count - 2) / 2;
                    for (int i = 2; i < 2 + divider; i++)
                        part1.Add(tosend[i]);
                    for (int i = 2 + divider; i < tosend.Count; i++)
                        part2.Add(tosend[i]);
                }

                ArrayList toAsk = new ArrayList();
                if (!knownUsers.ContainsKey((string)tosend[0]))
                {
                    if (res.hasKnownUser((string)tosend[0]))
                        this.knownUsers.Add((string)tosend[0], res.getUser((string)tosend[0]));
                    else
                        toAsk.Add(tosend[0]);
                }
                if (!knownUsers.ContainsKey((string)tosend[1]))
                {
                    if (res.hasKnownUser((string)tosend[1]))
                        this.knownUsers.Add((string)tosend[1], res.getUser((string)tosend[1]));
                    else
                        toAsk.Add(tosend[1]);
                }

                if (toAsk.Count != 0)
                {
                    serverOperations = chooseServer();
                    IDictionary<string, User> addrs = serverOperations.getUsers(toAsk);
                    Console.WriteLine("Communicated with the servers...");
                    if (addrs == null)
                    {
                        Console.WriteLine("Some user disconected");
                        if (!res.State.Equals("Aborted"))
                        {
                            initiateAbortion(res, null);
                        } 
                        return;
                    }
                    else
                    {
                        foreach (KeyValuePair<string, User> pair in addrs)
                            knownUsers.Add(pair.Key, pair.Value);
                    }
                }

                res.Propagation = part1;
                if (sendReservation(res, (string)tosend[0]))
                {
                    res.Propagation = part2;
                    if (!sendReservation(res, (string)tosend[1]))
                    {
                        initiateAbortion(res, null);
                    }
                }
                else
                {
                    initiateAbortion(res, null);
                }
            }
        }

        private bool sendReservation(Reservation res, string user)
        {
            try
            {
                clientOperations = (IClientService)Activator.GetObject(typeof(IClientService),
                "tcp://" + knownUsers[user].getAddress() + "/ClientService");

                clientOperations.receiveReservation(res);
                Console.WriteLine("Sent reservation {0} to user {1} in state ", res.Name, user, res.State);
            }
            catch (RemotingException)
            {
                serverOperations = chooseServer();
                User usr = serverOperations.getUser(user);
                Console.WriteLine("Communicated with the servers...");
                if (usr == null)
                {
                    return false;
                }
                else
                {
                    knownUsers[usr.Name] = usr;
                    return sendReservation(res, usr.Name);
                }
            }
            return true;
        }

        public bool sendAck(Ack ack, string user)
        {
            try
            {
                clientOperations = (IClientService)Activator.GetObject(typeof(IClientService),
                "tcp://" + knownUsers[user].getAddress() + "/ClientService");

                clientOperations.collectAck(ack);
            }
            catch (RemotingException)
            {
                serverOperations = chooseServer();
                Console.WriteLine("Comunicated with the server........");
                User usr = serverOperations.getUser(user);
                if (usr == null)
                {
                    return false;
                }
                else
                {
                    knownUsers[usr.Name] = usr;
                    return sendAck(ack, usr.Name);
                }
            }
            return true;
        }

        public ArrayList getSlotReservations(int index)
        {
            return slots[index].getAllReservations();
        }
    }
}